@fieldwangai/agentflow 0.1.69 → 0.1.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/lib/apply.mjs +35 -2
- package/bin/lib/catalog-flows.mjs +1 -0
- package/bin/lib/locales/en.json +8 -0
- package/bin/lib/locales/zh.json +8 -0
- package/bin/lib/marketplace.mjs +1 -0
- package/bin/lib/paths.mjs +2 -0
- package/bin/lib/run-ledger.mjs +96 -0
- package/bin/lib/ui-server.mjs +782 -47
- package/bin/lib/wecom.mjs +102 -0
- package/bin/pipeline/pre-process-node.mjs +40 -8
- package/builtin/nodes/tool_wecom_send_app_markdown.md +56 -0
- package/builtin/nodes/tool_wecom_send_group_markdown.md +44 -0
- package/builtin/web-ui/dist/assets/index-B3Wr-I3p.css +1 -0
- package/builtin/web-ui/dist/assets/index-C8JMDUZb.js +296 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/builtin/web-ui/dist/assets/index-CCvxfovU.css +0 -1
- package/builtin/web-ui/dist/assets/index-DvdPaPBL.js +0 -296
package/bin/lib/apply.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import { printEntryAndFlowFiles, printNodeStatusTable, runValidateFlowAndExitIfI
|
|
|
24
24
|
import { clearApplyActiveLock, writeApplyActiveLock } from "./run-apply-active-lock.mjs";
|
|
25
25
|
import { ensureReference, findFlowNameByUuid, getFlowDir, getRunDir } from "./workspace.mjs";
|
|
26
26
|
import { readMergedEnvObject } from "./user-env.mjs";
|
|
27
|
+
import { appendRunLedgerEvent } from "./run-ledger.mjs";
|
|
27
28
|
|
|
28
29
|
const PARALLEL_PREFIX_COLORS = [
|
|
29
30
|
(s) => chalk.cyan(s),
|
|
@@ -72,6 +73,31 @@ export async function apply(workspaceRoot, flowName, uuidArg, dryRun, agentModel
|
|
|
72
73
|
// 提前 ensure:apply-start 携带原始 run 起始时间,UI 计时器可按「这个 uuid 从头到现在的总时长」显示,
|
|
73
74
|
// 避免每次 resume 仅从 totalExecutedMs 累加看起来像从 resume 才开始计时。
|
|
74
75
|
const priorRunStartTime = ensureRunStartTime(workspaceRoot, flowName, uuid);
|
|
76
|
+
let runStartTime = priorRunStartTime;
|
|
77
|
+
let totalExecutedMs = priorTotalExecutedMs;
|
|
78
|
+
const writeRunLedger = !dryRun;
|
|
79
|
+
let runLedgerFinished = false;
|
|
80
|
+
const runLedgerBase = {
|
|
81
|
+
kind: "pipeline",
|
|
82
|
+
runId: uuid,
|
|
83
|
+
userId: String(process.env.AGENTFLOW_USER_ID || ""),
|
|
84
|
+
username: String(process.env.AGENTFLOW_USER_ID || ""),
|
|
85
|
+
flowId: flowName,
|
|
86
|
+
flowSource: "user",
|
|
87
|
+
at: priorRunStartTime,
|
|
88
|
+
};
|
|
89
|
+
if (writeRunLedger) appendRunLedgerEvent({ ...runLedgerBase, type: "run_started" });
|
|
90
|
+
const finishRunLedger = (status) => {
|
|
91
|
+
if (!writeRunLedger || runLedgerFinished) return;
|
|
92
|
+
runLedgerFinished = true;
|
|
93
|
+
appendRunLedgerEvent({
|
|
94
|
+
...runLedgerBase,
|
|
95
|
+
type: "run_finished",
|
|
96
|
+
endedAt: Date.now(),
|
|
97
|
+
durationMs: totalExecutedMs,
|
|
98
|
+
status,
|
|
99
|
+
});
|
|
100
|
+
};
|
|
75
101
|
emitEvent(workspaceRoot, flowName, uuid, {
|
|
76
102
|
event: "apply-start",
|
|
77
103
|
flowName,
|
|
@@ -85,8 +111,6 @@ export async function apply(workspaceRoot, flowName, uuidArg, dryRun, agentModel
|
|
|
85
111
|
writeApplyActiveLock(workspaceRoot, flowName, uuid);
|
|
86
112
|
|
|
87
113
|
try {
|
|
88
|
-
let runStartTime = priorRunStartTime;
|
|
89
|
-
let totalExecutedMs = priorTotalExecutedMs;
|
|
90
114
|
let round = 0;
|
|
91
115
|
while (round < MAX_LOOP_ROUNDS) {
|
|
92
116
|
round++;
|
|
@@ -100,6 +124,7 @@ export async function apply(workspaceRoot, flowName, uuidArg, dryRun, agentModel
|
|
|
100
124
|
if (readyNodes.length === 0) {
|
|
101
125
|
if (allDone) {
|
|
102
126
|
saveTotalExecutedMs(workspaceRoot, flowName, uuid, totalExecutedMs);
|
|
127
|
+
finishRunLedger("success");
|
|
103
128
|
const totalElapsed = formatDuration(totalExecutedMs);
|
|
104
129
|
emitEvent(workspaceRoot, flowName, uuid, {
|
|
105
130
|
event: "apply-done",
|
|
@@ -163,6 +188,7 @@ export async function apply(workspaceRoot, flowName, uuidArg, dryRun, agentModel
|
|
|
163
188
|
if (options.length === 0) {
|
|
164
189
|
log.info(chalk.yellow(`节点 ${pendId} 未配置任何选项(output 槽位),无法选择。请先在编辑器中添加 output 槽位。`));
|
|
165
190
|
log.info(chalk.bold.yellow("→ " + t("flow.resume_hint") + " ") + resumeExample);
|
|
191
|
+
finishRunLedger("interrupted");
|
|
166
192
|
return;
|
|
167
193
|
}
|
|
168
194
|
|
|
@@ -177,6 +203,7 @@ export async function apply(workspaceRoot, flowName, uuidArg, dryRun, agentModel
|
|
|
177
203
|
}
|
|
178
204
|
process.stderr.write(chalk.bold.cyan("━━━━━━━━━━━━━━━━━━━━━━━━━\n"));
|
|
179
205
|
log.info(chalk.bold.yellow("→ " + t("flow.resume_hint") + " ") + resumeExample);
|
|
206
|
+
finishRunLedger("interrupted");
|
|
180
207
|
return;
|
|
181
208
|
}
|
|
182
209
|
|
|
@@ -209,6 +236,7 @@ export async function apply(workspaceRoot, flowName, uuidArg, dryRun, agentModel
|
|
|
209
236
|
if (trimmed === "q") {
|
|
210
237
|
rl.close();
|
|
211
238
|
log.info(chalk.yellow("用户取消,流程暂停。") + chalk.dim(` 恢复命令: ${resumeExample}`));
|
|
239
|
+
finishRunLedger("interrupted");
|
|
212
240
|
return;
|
|
213
241
|
}
|
|
214
242
|
const idx = parseInt(trimmed, 10);
|
|
@@ -250,6 +278,7 @@ export async function apply(workspaceRoot, flowName, uuidArg, dryRun, agentModel
|
|
|
250
278
|
}
|
|
251
279
|
process.stderr.write(chalk.bold.cyan("━━━━━━━━━━━━━━━━━━━━━━━━━\n"));
|
|
252
280
|
log.info(chalk.bold.yellow("→ " + t("flow.resume_hint") + " ") + resumeExample);
|
|
281
|
+
finishRunLedger("interrupted");
|
|
253
282
|
return;
|
|
254
283
|
}
|
|
255
284
|
|
|
@@ -380,6 +409,7 @@ ${currentContent}
|
|
|
380
409
|
} else if (answer.trim().toLowerCase() === "q") {
|
|
381
410
|
rl.close();
|
|
382
411
|
log.info(chalk.yellow("用户取消,流程暂停。") + chalk.dim(` 恢复命令: ${resumeExample}`));
|
|
412
|
+
finishRunLedger("interrupted");
|
|
383
413
|
return;
|
|
384
414
|
}
|
|
385
415
|
}
|
|
@@ -393,6 +423,7 @@ ${currentContent}
|
|
|
393
423
|
}
|
|
394
424
|
|
|
395
425
|
log.info(chalk.bold.yellow("→ " + t("flow.resume_hint") + " ") + resumeExample);
|
|
426
|
+
finishRunLedger("interrupted");
|
|
396
427
|
return;
|
|
397
428
|
}
|
|
398
429
|
const endNodeIds = Array.isArray(parseOut.nodes)
|
|
@@ -401,6 +432,7 @@ ${currentContent}
|
|
|
401
432
|
const endReached = endNodeIds.some((id) => instanceStatus[id] === "success");
|
|
402
433
|
if (endReached) {
|
|
403
434
|
saveTotalExecutedMs(workspaceRoot, flowName, uuid, totalExecutedMs);
|
|
435
|
+
finishRunLedger("success");
|
|
404
436
|
const totalElapsed = formatDuration(totalExecutedMs);
|
|
405
437
|
emitEvent(workspaceRoot, flowName, uuid, {
|
|
406
438
|
event: "apply-done",
|
|
@@ -763,6 +795,7 @@ ${currentContent}
|
|
|
763
795
|
maxErr.uuid = uuid;
|
|
764
796
|
throw maxErr;
|
|
765
797
|
} finally {
|
|
798
|
+
finishRunLedger("failed");
|
|
766
799
|
clearApplyActiveLock(workspaceRoot, flowName, uuid);
|
|
767
800
|
}
|
|
768
801
|
}
|
|
@@ -158,6 +158,7 @@ function normalizeFrontmatterSlots(arr) {
|
|
|
158
158
|
else if (typeof def !== "string") def = String(def);
|
|
159
159
|
const slot = { type, name, default: def };
|
|
160
160
|
if (item.required != null) slot.required = Boolean(item.required);
|
|
161
|
+
if (item.description != null) slot.description = String(item.description);
|
|
161
162
|
slot.showOnNode = item.showOnNode != null ? Boolean(item.showOnNode) : defaultShowOnNodeForSlot(slot);
|
|
162
163
|
return slot;
|
|
163
164
|
});
|
package/bin/lib/locales/en.json
CHANGED
|
@@ -289,6 +289,14 @@
|
|
|
289
289
|
"displayName": "Create GitLab MR",
|
|
290
290
|
"description": "Create or reuse a GitLab merge request for the current branch and output its URL"
|
|
291
291
|
},
|
|
292
|
+
"tool_wecom_send_group_markdown": {
|
|
293
|
+
"displayName": "WeCom Group Markdown",
|
|
294
|
+
"description": "Send a Markdown message through a WeCom group robot webhook"
|
|
295
|
+
},
|
|
296
|
+
"tool_wecom_send_app_markdown": {
|
|
297
|
+
"displayName": "WeCom App Markdown",
|
|
298
|
+
"description": "Send a Markdown message to users through a WeCom enterprise application"
|
|
299
|
+
},
|
|
292
300
|
"tool_nodejs": {
|
|
293
301
|
"displayName": "Node.js Script",
|
|
294
302
|
"description": "Execute Node.js script, success determined by exit code, stdout as result"
|
package/bin/lib/locales/zh.json
CHANGED
|
@@ -289,6 +289,14 @@
|
|
|
289
289
|
"displayName": "提交 GitLab MR",
|
|
290
290
|
"description": "为当前分支创建或复用 GitLab Merge Request,并输出 MR 链接"
|
|
291
291
|
},
|
|
292
|
+
"tool_wecom_send_group_markdown": {
|
|
293
|
+
"displayName": "企业微信群 Markdown",
|
|
294
|
+
"description": "通过企业微信群机器人 webhook 发送 Markdown 消息"
|
|
295
|
+
},
|
|
296
|
+
"tool_wecom_send_app_markdown": {
|
|
297
|
+
"displayName": "企业微信应用 Markdown",
|
|
298
|
+
"description": "通过企业微信应用消息接口发送 Markdown 给用户"
|
|
299
|
+
},
|
|
292
300
|
"tool_nodejs": {
|
|
293
301
|
"displayName": "Node.js 脚本",
|
|
294
302
|
"description": "执行 Node.js 脚本,以 exit code 判断成败,stdout 作为结果"
|
package/bin/lib/marketplace.mjs
CHANGED
|
@@ -72,6 +72,7 @@ function normalizeSlotList(value) {
|
|
|
72
72
|
default: def == null ? "" : String(def),
|
|
73
73
|
};
|
|
74
74
|
if (slot.required != null) normalized.required = Boolean(slot.required);
|
|
75
|
+
if (slot.description != null) normalized.description = String(slot.description);
|
|
75
76
|
normalized.showOnNode = slot.showOnNode != null
|
|
76
77
|
? Boolean(slot.showOnNode)
|
|
77
78
|
: Boolean(normalized.required) || type.toLowerCase() === "node";
|
package/bin/lib/paths.mjs
CHANGED
|
@@ -302,6 +302,8 @@ export const LOCAL_ONLY_DEFINITION_IDS = new Set([
|
|
|
302
302
|
"tool_git_worktree_load",
|
|
303
303
|
"tool_git_worktree_unload",
|
|
304
304
|
"tool_gitlab_create_mr",
|
|
305
|
+
"tool_wecom_send_group_markdown",
|
|
306
|
+
"tool_wecom_send_app_markdown",
|
|
305
307
|
"tool_print",
|
|
306
308
|
"tool_user_check",
|
|
307
309
|
"tool_user_ask",
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import crypto from "crypto";
|
|
4
|
+
|
|
5
|
+
import { getAgentflowDataRoot } from "./paths.mjs";
|
|
6
|
+
|
|
7
|
+
function localDayKey(timeMs) {
|
|
8
|
+
const d = new Date(Number(timeMs) || Date.now());
|
|
9
|
+
const y = d.getFullYear();
|
|
10
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
11
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
12
|
+
return `${y}-${m}-${day}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function safeSegment(value, fallback = "run") {
|
|
16
|
+
return String(value || fallback)
|
|
17
|
+
.trim()
|
|
18
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "_")
|
|
19
|
+
.replace(/^_+|_+$/g, "")
|
|
20
|
+
.slice(0, 120) || fallback;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function runLedgerId(prefix = "run") {
|
|
24
|
+
return `${safeSegment(prefix, "run")}-${Date.now().toString(36)}-${crypto.randomBytes(4).toString("hex")}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function runLedgerDir() {
|
|
28
|
+
return path.join(getAgentflowDataRoot(), "admin", "run-ledger");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function runLedgerPath(timeMs = Date.now()) {
|
|
32
|
+
return path.join(runLedgerDir(), `${localDayKey(timeMs)}.jsonl`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function appendRunLedgerEvent(event = {}) {
|
|
36
|
+
try {
|
|
37
|
+
const item = {
|
|
38
|
+
version: 1,
|
|
39
|
+
type: String(event.type || ""),
|
|
40
|
+
kind: String(event.kind || ""),
|
|
41
|
+
runId: String(event.runId || runLedgerId()),
|
|
42
|
+
userId: String(event.userId || ""),
|
|
43
|
+
username: String(event.username || event.userId || ""),
|
|
44
|
+
flowId: String(event.flowId || ""),
|
|
45
|
+
flowSource: String(event.flowSource || "user"),
|
|
46
|
+
runNodeId: String(event.runNodeId || ""),
|
|
47
|
+
at: Number(event.at || event.startedAt || Date.now()),
|
|
48
|
+
endedAt: event.endedAt == null ? null : Number(event.endedAt),
|
|
49
|
+
durationMs: Math.max(0, Number(event.durationMs || 0)),
|
|
50
|
+
status: event.status ? String(event.status || "") : "",
|
|
51
|
+
};
|
|
52
|
+
if (!item.type || !item.kind || !item.runId || !item.flowId) return;
|
|
53
|
+
const filePath = runLedgerPath(item.at);
|
|
54
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
55
|
+
fs.appendFileSync(filePath, JSON.stringify(item) + "\n", "utf-8");
|
|
56
|
+
} catch {
|
|
57
|
+
// Usage telemetry must never affect the run itself.
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readJsonlObjects(filePath) {
|
|
62
|
+
if (!fs.existsSync(filePath)) return [];
|
|
63
|
+
try {
|
|
64
|
+
const out = [];
|
|
65
|
+
const lines = fs.readFileSync(filePath, "utf-8").split(/\r?\n/);
|
|
66
|
+
for (const line of lines) {
|
|
67
|
+
if (!line.trim()) continue;
|
|
68
|
+
try {
|
|
69
|
+
const parsed = JSON.parse(line);
|
|
70
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) out.push(parsed);
|
|
71
|
+
} catch {
|
|
72
|
+
/* ignore malformed line */
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
} catch {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function readRunLedgerEvents(options = {}) {
|
|
82
|
+
const dir = runLedgerDir();
|
|
83
|
+
if (!fs.existsSync(dir)) return [];
|
|
84
|
+
try {
|
|
85
|
+
const sinceMs = Number(options?.sinceMs || 0);
|
|
86
|
+
const sinceKey = Number.isFinite(sinceMs) && sinceMs > 0 ? localDayKey(sinceMs) : "";
|
|
87
|
+
const files = fs.readdirSync(dir, { withFileTypes: true })
|
|
88
|
+
.filter((entry) => entry.isFile() && /^\d{4}-\d{2}-\d{2}\.jsonl$/.test(entry.name))
|
|
89
|
+
.filter((entry) => !sinceKey || entry.name.slice(0, 10) >= sinceKey)
|
|
90
|
+
.map((entry) => path.join(dir, entry.name))
|
|
91
|
+
.sort();
|
|
92
|
+
return files.flatMap((filePath) => readJsonlObjects(filePath));
|
|
93
|
+
} catch {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
}
|